const std = @import("std");
const net = std.net;
const posix = std.posix;

pub const SSDP_PORT: u16 = 1900;

pub const ParseError = error{
    UnknownMethod,
    HeadersInvalid,
    HeaderContinuationsUnsupported,
    TransferEncodingUnsupported,
    ConnectionHeaderUnsupported,
    InvalidContentLength,
    CompressionUnsupported,
    MissingFinalNewline,
};

pub const Method = enum(u64) {
    MSEARCH = std.http.Method.parse("M-SEARCH"),
    NOTIFY = std.http.Method.parse("NOTIFY"),

    pub fn parseHttpMethod(method: std.http.Method) !Method {
        const i = @intFromEnum(method);
        const type_info = @typeInfo(Method);

        inline for (type_info.Enum.fields) |field| {
            if (field.value == i) {
                return @enumFromInt(field.value);
            }
        }

        return ParseError.UnknownMethod;
    }

    pub fn parse(s: []const u8) ParseError!Method {
        if (s.len > 8) {
            return ParseError.UnknownMethod;
        }

        const i = std.http.Method.parse(s);
        const type_info = @typeInfo(Method);

        inline for (type_info.Enum.fields) |field| {
            if (field.value == i) {
                return @enumFromInt(i);
            }
        }

        return ParseError.UnknownMethod;
    }

    pub fn write(self: Method, writer: anytype) !void {
        const bytes = std.mem.asBytes(&@intFromEnum(self));
        const str = std.mem.sliceTo(bytes, 0);
        try writer.writeAll(str);
    }

    pub fn format(self: Method, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
        _ = fmt;
        _ = options;

        try self.write(writer);
    }
};

pub const MSearchHead = struct {
    mx: u32 = 0,
    st: []const u8 = "",
    host: net.Address = undefined,

    pub fn format(self: MSearchHead, comptime fmt: []const u8, options: std.fmt.FormatOptions, out_stream: anytype) !void {
        _ = options;
        if (fmt.len != 0) {
            std.fmt.invalidFmtError(fmt, self);
        }

        try std.fmt.format(out_stream, "ST: {s}, MX: {d}", .{ self.st, self.mx });
    }
};

pub const MSearchRequest = struct {
    buf: [1024]u8 = undefined,
    sock: posix.socket_t = undefined,
    head: MSearchHead = undefined,
    // src_addr: posix.sockaddr = undefined,
    // addr_len: posix.socklen_t = undefined,
};

pub fn newUDPSocket() !posix.socket_t {
    const sock = try posix.socket(
        posix.AF.INET,
        posix.SOCK.DGRAM,
        posix.IPPROTO.UDP,
    );

    try posix.setsockopt(sock, posix.SOL.SOCKET, posix.SO.REUSEPORT, &std.mem.toBytes(@as(c_int, 1)));
    try posix.setsockopt(sock, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)));
    try posix.setsockopt(sock, posix.IPPROTO.IP, posix.system.IP.MULTICAST_LOOP, &std.mem.toBytes(@as(c_int, 0)));
    try posix.setsockopt(sock, posix.IPPROTO.IP, posix.system.IP.MULTICAST_TTL, &std.mem.toBytes(@as(c_int, 2)));

    return sock;
}

// impl struct ip_mreq in "netinet/in.h"
const IpMreq = struct {
    multiaddr: u32,
    interface: u32 = INADDR_ANY,

    const INADDR_ANY: u32 = 0;

    pub fn init(name: []const u8) IpMreq {
        const ip = std.net.Ip4Address.resolveIp(name, 0) catch unreachable;
        return IpMreq{ .multiaddr = ip.sa.addr };
    }
};

pub fn listenner(DeviceType: type) type {
    return struct {
        alloctor: std.mem.Allocator,
        device: *DeviceType,
        port: u16 = SSDP_PORT,
        address: net.Address = undefined,
        sock: posix.socket_t = undefined,
        running: bool = false,
        thread: std.Thread = undefined,

        const Self = @This();

        pub fn init(self: *Self) !void {

            // adjust the ip/port here as needed
            self.address = try net.Address.parseIp("0.0.0.0", self.port);

            // get a socket and set domain, type and protocol flags
            self.sock = try newUDPSocket();

            // 加入组播以便可以接收组里的广播
            const mreq = IpMreq.init("239.255.255.250");
            try posix.setsockopt(self.sock, posix.IPPROTO.IP, posix.system.IP.ADD_MEMBERSHIP, &std.mem.toBytes(mreq));

            try posix.bind(self.sock, &self.address.any, self.address.getOsSockLen());
        }

        fn listen(self: *Self) void {
            self.running = true;
            std.log.debug("Listening on {any}...", .{self.address});
            while (self.running) {
                var buf: [1024]u8 = undefined;
                var src_addr: posix.sockaddr = undefined;
                var addr_len: posix.socklen_t = @sizeOf(posix.sockaddr);

                const size = posix.recvfrom(self.sock, &buf, 0, &src_addr, &addr_len) catch |e| {
                    std.log.warn("fail to listen: {any}", .{e});
                    continue;
                };

                var method: Method = undefined;
                var version: std.http.Version = undefined;
                self.parse(buf[0..size], &method, &version) catch |e| {
                    std.log.warn("Parse Method Error: {any}", .{e});
                    continue;
                };

                // std.log.debug("listening method: {any}, version: {any}", .{ method, version });
                if (method == Method.MSEARCH) {
                    const head = self.parseMSEARCH(buf[0..size]) catch |e| {
                        std.log.warn("Parse MSEARCH Error: {any}", .{e});
                        continue;
                    };

                    std.log.debug("{any}: {any}", .{ method, head });
                    if (head.st.len == 0) {
                        continue;
                    }

                    self.onMSEARCH(head, &src_addr, addr_len);
                }
            }

            std.log.debug("stop listen", .{});
        }

        pub fn start(self: *Self) !void {
            self.thread = try std.Thread.spawn(.{ .allocator = self.alloctor }, Self.listen, .{self});
        }

        pub fn stop(self: *Self) void {
            self.running = false;
            posix.close(self.sock);
            self.thread.join();
        }

        fn parse(_: Self, bytes: []const u8, method: *Method, version: *std.http.Version) ParseError!void {
            const first_line_end = std.mem.indexOf(u8, bytes, "\r\n") orelse {
                return ParseError.HeadersInvalid;
            };

            const first_line = bytes[0..first_line_end];
            const method_end = std.mem.indexOfScalar(u8, first_line, ' ') orelse
                return ParseError.HeadersInvalid;

            const method_str = first_line[0..method_end];
            method.* = try Method.parse(method_str);

            const version_start = std.mem.lastIndexOfScalar(u8, first_line, ' ') orelse
                return ParseError.HeadersInvalid;
            if (version_start == method_end) {
                return ParseError.HeadersInvalid;
            }

            const version_str = first_line[version_start + 1 ..];
            if (version_str.len != 8) {
                return ParseError.HeadersInvalid;
            }

            version.* = switch (int64(version_str[0..8])) {
                int64("HTTP/1.0") => .@"HTTP/1.0",
                int64("HTTP/1.1") => .@"HTTP/1.1",
                else => return ParseError.HeadersInvalid,
            };
        }

        inline fn int64(array: *const [8]u8) u64 {
            return @bitCast(array.*);
        }

        fn parseMSEARCH(_: Self, bytes: []const u8) ParseError!MSearchHead {
            var head = MSearchHead{};
            var it = std.http.HeaderIterator.init(bytes);
            var have_host = false;
            var have_mx = false;
            var have_man = false;

            while (it.next()) |item| {
                if (std.mem.eql(u8, item.name, "HOST")) {
                    var ip_str: []const u8 = undefined;
                    var port: u16 = undefined;
                    const ip_end = std.mem.indexOfScalar(u8, item.value, ':');
                    if (ip_end) |end| {
                        ip_str = item.value[0..end];
                        const port_start = end + 1;
                        port = std.fmt.parseInt(u16, item.value[port_start..], 10) catch {
                            return ParseError.HeadersInvalid;
                        };
                    } else {
                        port = SSDP_PORT;
                    }

                    head.host = net.Address.parseIp(ip_str, port) catch {
                        return ParseError.HeadersInvalid;
                    };

                    have_host = true;
                } else if (std.mem.eql(u8, item.name, "MX")) {
                    head.mx = std.fmt.parseInt(u32, item.value, 10) catch {
                        return ParseError.HeadersInvalid;
                    };

                    have_mx = true;
                } else if (std.mem.eql(u8, item.name, "ST")) {
                    head.st = item.value;
                } else if (std.mem.eql(u8, item.name, "MAN")) {
                    if (std.mem.eql(u8, item.value, "\"ssdp:discover\"")) {
                        have_man = true;
                    }
                }
            }

            if (!have_host or !have_mx or !have_man) {
                return ParseError.HeadersInvalid;
            }

            return head;
        }

        fn onMSEARCH(self: *Self, head: MSearchHead, src_addr: *posix.sockaddr, addr_len: posix.socklen_t) void {
            var rand = std.crypto.random;
            const ns = rand.intRangeLessThan(u64, 0, head.mx * 1000_000_000);
            var tx_buf: [500]u8 = undefined;
            var h = std.ArrayListUnmanaged(u8).initBuffer(&tx_buf);

            self.device.onMSEARCH(h.fixedWriter(), head) catch {
                std.log.warn("fail to repsond M-SEARCH", .{});
                return;
            };

            if (h.items.len == 0) {
                return;
            }

            std.log.debug("sleep ns: {d}", .{ns});
            std.time.sleep(ns);

            {
                std.log.debug("repsond M-SEARCH: {s}", .{tx_buf[0..h.items.len]});
                const sock = newUDPSocket() catch {
                    std.log.err("fail to generate UDP socket!", .{});
                    return;
                };
                defer posix.close(sock);

                const repsond = tx_buf[0..h.items.len];
                _ = posix.sendto(sock, repsond, 0, src_addr, addr_len) catch {
                    std.log.err("UDP socket fail to send!", .{});
                };
            }
        }
    };
}
